<?php

/*--------------------------------------------------------------------------/
* @Author		KulerThemes.com http://www.kulerthemes.com
* @Copyright	Copyright (C) 2012 - 2013 KulerThemes.com. All rights reserved.
* @License		KulerThemes.com Proprietary License
/---------------------------------------------------------------------------*/

class ModelKulerCp extends Model {
    private $table_name = '';

    public function __construct($registry)
    {
        parent::__construct($registry);

        $this->table_name = DB_PREFIX . 'kulercp_resources';
    }

	public function editSetting($group, $key, $value, $store_id = 0) {
		$serialized = 0;
		
		if(is_array($value) || is_object($value)) {
			$value = serialize($value);
			$serialized = 1;
		}
        $store_id = intval($store_id);

		$this->db->query("DELETE FROM " . DB_PREFIX . "setting
                          WHERE `group` = '{$group}' AND `key` = '{$key}' AND `store_id` = $store_id");
		$this->db->query("INSERT INTO " . DB_PREFIX . "setting
                          SET `group` = '{$group}', `key` = '{$key}', `serialized` = '{$serialized}', `value` = '" . $this->db->escape($value) . "', `store_id` = $store_id");
	}
    
	public function editSettingValue($group = '', $key = '', $value = '', $store_id = 0) {
		if (!is_array($value)) {
			$this->db->query("UPDATE " . DB_PREFIX . "setting SET `value` = '" . $this->db->escape($value) . " WHERE `group` = '" . $this->db->escape($group) . "' AND `key` = '" . $this->db->escape($key) . "' AND store_id = '" . (int)$store_id . "'");
		} else {
			$this->db->query("UPDATE " . DB_PREFIX . "setting SET `value` = '" . $this->db->escape(serialize($value)) . "' WHERE `group` = '" . $this->db->escape($group) . "' AND `key` = '" . $this->db->escape($key) . "' AND store_id = '" . (int)$store_id . "' AND serialized = '1'");
		}
    }

	public function getCurrentLanguage() {
		$this->load->model('localisation/language');

		$languages = $this->model_localisation_language->getLanguages();
		foreach ($languages as $language) {
			if ($language['code'] == $this->session->data['language']) {
				return $language;
			}
		}
	}

    public function updateTable()
    {
        $table_data = array();

        $query = $this->db->query("SHOW TABLES FROM `" . DB_DATABASE . "`");

        foreach ($query->rows as $result) {
            if ($result['Tables_in_' . DB_DATABASE] == 'resources')
            {
                $this->db->query('ALTER TABLE  `resources` ADD COLUMN  `store_id` INT( 11 ) DEFAULT 0');
                $this->db->query("RENAME TABLE `resources` TO `{$this->table_name}`");

                return true;
            }
        }

        return false;
    }
 
	public function createTable() {
		// Create table
		$query = "CREATE TABLE IF NOT EXISTS `{$this->table_name}` (
			`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
			`store_id` int(11) DEFAULT 0,
			`type` varchar(10) NOT NULL DEFAULT '',
			`name` varchar(45) NOT NULL DEFAULT '',
			`router` varchar(1024) NOT NULL DEFAULT '',
			`version` int(10) unsigned NOT NULL,
			`files` longtext NOT NULL,
			`cache` longblob NOT NULL,
			`param` longtext NOT NULL,
			`status` int(11) DEFAULT NULL,
			PRIMARY KEY (`id`),
			UNIQUE KEY `type` (`type`)
		) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;";
		
		return $this->db->query($query);
	}

    public function dropTable()
    {
        $query = $this->db->query("SHOW TABLES FROM `" . DB_DATABASE . "`");

        foreach ($query->rows as $result) {
            if ($result['Tables_in_' . DB_DATABASE] == 'resources')
            {
                $this->db->query('DROP TABLE `resources`');
            }
            else if ($result['Tables_in_' . DB_DATABASE] == $this->table_name)
            {
                $this->db->query("DROP TABLE `{$this->table_name}`");
            }
        }
    }
	
	public function insertFields($fields, $store_id) {
		$table = $this->table_name;

		$insertFields = array();

		if (empty($fields)) {
			return false;
		}

		foreach ($fields as $name => $value) {
			if (strpos($name, '_') === 0) {
				continue;
			}
			$insertFields[] = '`' . $name . '` = \'' . $this->db->escape($value) . '\'';
		}

        $store_id = intval($store_id);
        $insertFields[] = '`store_id` = ' . $store_id;
		
		$insertQuery = implode(',', $insertFields);
		$query = ' INSERT INTO ' . $table . ' SET ';
		$query.= $insertQuery;
		$query.= ' ON DUPLICATE KEY UPDATE ';
		$query.= $insertQuery;

		$this->db->query($query);

		return $this->db->getLastId();
	}
	
	public function getCollections() {
		$query = $this->db->query("SELECT * FROM {$this->table_name} ORDER BY type DESC");
		$collection = array();
		if ($query->rows) {
			foreach ($query->rows as $row) {
				$row['router'] = $row['router'] ?  $row['router'] : '*';
				$row['files'] = unserialize($row['files']);
				$collection[] = $row;
			}
		}
		
		return $collection;
	}
	
	public function deleteResource($value, $field = 'id') {
		return $this->db->query("DELETE FROM {$this->table_name} WHERE `{$field}`='" . $this->db->escape($value) . "'");
	}
	
	public function getResource($rid) {
		$query = $this->db->query("SELECT * FROM {$this->table_name} WHERE id=" . $rid);
		return $query->row;
	}
    
    public function getResourceByType($type, $store_id) {
        $store_id = intval($store_id);

		$query = $this->db->query("SELECT * FROM {$this->table_name} WHERE type='{$type}' AND `store_id` = $store_id");
		return $query->row;
	}
	
	public function updateResource($router, $param, $content, $status, $rid) {
		$this->db->query("UPDATE {$this->table_name} SET router='" . $this->db->escape($router) . "', param='" . $this->db->escape($param) . "', cache='" . $this->db->escape($content) . "', version=version+1, status=$status WHERE id=$rid");
	}
   
    /**
     * @category Sitemap helper function
     */
    public function getSitemapCategories($parent_id = 0) {
        $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "category c LEFT JOIN " . DB_PREFIX . "category_description cd ON (c.category_id = cd.category_id) LEFT JOIN " . DB_PREFIX . "category_to_store c2s ON (c.category_id = c2s.category_id) WHERE c.parent_id = '" . (int) $parent_id . "' AND cd.language_id = '" . (int) $this->config->get('config_language_id') . "' AND c2s.store_id = '" . (int) $this->config->get('config_store_id') . "'  AND c.status = '1' ORDER BY c.sort_order, LCASE(cd.name)");

        return $query->rows;
    }
	
    public function getAllProducts($data = array()) {
        $sql = "SELECT p.* FROM " . DB_PREFIX . "product p LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id) WHERE pd.language_id = '" . (int) $this->config->get('config_language_id') . "'";

        if (isset($data['filter_name']) && !is_null($data['filter_name'])) {
            $sql .= " AND LCASE(pd.name) LIKE '%" . $this->db->escape(strtolower($data['filter_name'])) . "%'";
        }

        if (isset($data['filter_model']) && !is_null($data['filter_model'])) {
            $sql .= " AND LCASE(p.model) LIKE '%" . $this->db->escape(strtolower($data['filter_model'])) . "%'";
        }

        if (isset($data['filter_quantity']) && !is_null($data['filter_quantity'])) {
            $sql .= " AND p.quantity = '" . $this->db->escape($data['filter_quantity']) . "'";
        }

        if (isset($data['filter_status']) && !is_null($data['filter_status'])) {
            $sql .= " AND p.status = '" . (int) $data['filter_status'] . "'";
        }
        
        if (isset($data['sort'])) {
			$sql .= " ORDER BY " . $data['sort'];	
            if (isset($data['order']) && ($data['order'] == 'DESC')) {
                $sql .= " DESC";
            } else {
                $sql .= " ASC";
            }
        }
		
		if (isset($data['limit'])) {
			if ($data['limit'] < 1) {
				$data['limit'] = 20;
			}	
		
			$sql .= " LIMIT " . (int)$data['limit'];
		}	

        $query = $this->db->query($sql);

        return $query->rows;
    }
        
    public function rewrite($link) {
        if ($this->config->get('config_seo_url')) {
            $url_data = parse_url(str_replace('&amp;', '&', $link));

            $url = '';

            $data = array();

            parse_str($url_data['query'], $data);

            foreach ($data as $key => $value) {
                if (($key == 'product_id') || ($key == 'manufacturer_id') || ($key == 'information_id')) {
                    $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "url_alias WHERE `query` = '" . $this->db->escape($key . '=' . (int) $value) . "'");

                    if ($query->num_rows) {
                        $url .= '/' . $query->row['keyword'];

                        unset($data[$key]);
                    }
                } elseif ($key == 'path') {
                    $categories = explode('_', $value);

                    foreach ($categories as $category) {
                        $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "url_alias WHERE `query` = 'category_id=" . (int) $category . "'");

                        if ($query->num_rows) {
                            $url .= '/' . $query->row['keyword'];
                        }
                    }

                    unset($data[$key]);
                }
            }

            if ($url) {
                unset($data['route']);

                $query = '';

                if ($data) {
                    foreach ($data as $key => $value) {
                        $query .= '&' . $key . '=' . $value;
                    }

                    if ($query) {
                        $query = '?' . trim($query, '&');
                    }
                }

                return $url_data['scheme'] . '://' . $url_data['host'] . (isset($url_data['port']) ? ':' . $url_data['port'] : '') . str_replace('/index.php', '', $url_data['path']) . $url . $query;
            } else {
                return $link;
            }
        } else {
            return $link;
        }
    }

    public function exportBeforeInstallSampleData()
    {
        $output = '';

        $user_table = DB_PREFIX . 'user';

        $tables = array(
            DB_PREFIX . 'user' => '',
            DB_PREFIX . 'setting' => "`key` IN ('config_url', 'config_encryption')"
        );

        foreach ($tables as $table => $condition)
        {
            $sql = "SELECT * FROM `" . $table . "`";

            if ($condition)
            {
                $sql .= ' WHERE ' . $condition;
            }

            $query = $this->db->query($sql);

            foreach ($query->rows as $result) {
                $fields = '';

                foreach (array_keys($result) as $value) {
                    $fields .= '`' . $value . '`, ';
                }

                $values = '';

                foreach (array_values($result) as $value)
                {
                    $value = str_replace(array("\x00", "\x0a", "\x0d", "\x1a"), array('\0', '\n', '\r', '\Z'), $value);
                    $value = str_replace(array("\n", "\r", "\t"), array('\n', '\r', '\t'), $value);
                    $value = str_replace('\\', '\\\\',	$value);
                    $value = str_replace('\'', '\\\'',	$value);
                    $value = str_replace('\\\n', '\n',	$value);
                    $value = str_replace('\\\r', '\r',	$value);
                    $value = str_replace('\\\t', '\t',	$value);

                    $values .= '\'' . $value . '\', ';
                }

                $output .= 'INSERT INTO `' . $table . '` (' . preg_replace('/, $/', '', $fields) . ') VALUES (' . preg_replace('/, $/', '', $values) . ');' . "\n";
            }

            $output .= "\n\n";
        }

        return $output;
    }

    public function restoreAfterInstallSampleData()
    {
        $db = $this->db;

        $db->query("DELETE FROM `" . DB_PREFIX . "setting` WHERE `key` = 'config_url'");
        $db->query("DELETE FROM `" . DB_PREFIX . "setting` WHERE `key` = 'config_encryption'");

        $db->query("TRUNCATE TABLE `" . DB_PREFIX . "user`");
    }
}

/**
 * @todo Module Helper Class For Build URL
 */
class router {
	public static function linkToAbsolute($baseUrl, $relativeUrl) {
		// If relative URL has a scheme, clean path and return.
		$r = self::linkSplit($relativeUrl);
		if ($r === FALSE)
			return FALSE;
		if (!empty($r['scheme'])) {
			if (!empty($r['path']) && $r['path'][0] == '/')
				$r['path'] = self::linkRemoveDot($r['path']);
			return self::linkJoin($r);
		}

		// Make sure the base URL is absolute.
		$b = self::linkSplit($baseUrl);
		if ($b === FALSE || empty($b['scheme']) || empty($b['host']))
			return FALSE;
		$r['scheme'] = $b['scheme'];

		// If relative URL has an authority, clean path and return.
		if (isset($r['host'])) {
			if (!empty($r['path']))
				$r['path'] = self::linkRemoveDot($r['path']);
			return self::linkJoin($r);
		}
		unset($r['port']);
		unset($r['user']);
		unset($r['pass']);

		// Copy base authority.
		$r['host'] = $b['host'];
		if (isset($b['port']))
			$r['port'] = $b['port'];
		if (isset($b['user']))
			$r['user'] = $b['user'];
		if (isset($b['pass']))
			$r['pass'] = $b['pass'];

		// If relative URL has no path, use base path
		if (empty($r['path'])) {
			if (!empty($b['path']))
				$r['path'] = $b['path'];
			if (!isset($r['query']) && isset($b['query']))
				$r['query'] = $b['query'];
			return self::linkJoin($r);
		}

		// If relative URL path doesn't start with /, merge with base path
		if ($r['path'][0] != '/') {
			$base = mb_strrchr($b['path'], '/', TRUE, 'UTF-8');
			if ($base === FALSE)
				$base = '';
			$r['path'] = $base . '/' . $r['path'];
		}
		$r['path'] = self::linkRemoveDot($r['path']);
		return self::linkJoin($r);
	}
	
	public static function linkRemoveDot($path) {
		// multi-byte character explode
		$inSegs = preg_split('!/!u', $path);
		$outSegs = array();
		foreach ($inSegs as $seg) {
			if ($seg == '' || $seg == '.')
				continue;
			if ($seg == '..')
				array_pop($outSegs);
			else
				array_push($outSegs, $seg);
		}
		$outPath = implode('/', $outSegs);
		if ($path[0] == '/')
			$outPath = '/' . $outPath;
		// compare last multi-byte character against '/'
		if ($outPath != '/' &&
				(mb_strlen($path) - 1) == mb_strrpos($path, '/', 'UTF-8'))
			$outPath .= '/';
		return $outPath;
	}
	
	public static function linkSplit($url, $decode = TRUE) {
		// Character sets from RFC3986.
		$xunressub = 'a-zA-Z\d\-._~\!$&\'()*+,;=';
		$xpchar = $xunressub . ':@%';

		// Scheme from RFC3986.
		$xscheme = '([a-zA-Z][a-zA-Z\d+-.]*)';

		// User info (user + password) from RFC3986.
		$xuserinfo = '(([' . $xunressub . '%]*)' .
				'(:([' . $xunressub . ':%]*))?)';

		// IPv4 from RFC3986 (without digit constraints).
		$xipv4 = '(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})';

		// IPv6 from RFC2732 (without digit and grouping constraints).
		$xipv6 = '(\[([a-fA-F\d.:]+)\])';

		// Host name from RFC1035.  Technically, must start with a letter.
		// Relax that restriction to better parse URL structure, then
		// leave host name validation to application.
		$xhost_name = '([a-zA-Z\d-.%]+)';

		// Authority from RFC3986.  Skip IP future.
		$xhost = '(' . $xhost_name . '|' . $xipv4 . '|' . $xipv6 . ')';
		$xport = '(\d*)';
		$xauthority = '((' . $xuserinfo . '@)?' . $xhost .
				'?(:' . $xport . ')?)';

		// Path from RFC3986.  Blend absolute & relative for efficiency.
		$xslash_seg = '(/[' . $xpchar . ']*)';
		$xpath_authabs = '((//' . $xauthority . ')((/[' . $xpchar . ']*)*))';
		$xpath_rel = '([' . $xpchar . ']+' . $xslash_seg . '*)';
		$xpath_abs = '(/(' . $xpath_rel . ')?)';
		$xapath = '(' . $xpath_authabs . '|' . $xpath_abs .
				'|' . $xpath_rel . ')';

		// Query and fragment from RFC3986.
		$xqueryfrag = '([' . $xpchar . '/?' . ']*)';

		// URL.
		$xurl = '^(' . $xscheme . ':)?' . $xapath . '?' .
				'(\?' . $xqueryfrag . ')?(#' . $xqueryfrag . ')?$';


		// Split the URL into components.
		if (!preg_match('!' . $xurl . '!', $url, $m))
			return FALSE;

		if (!empty($m[2]))
			$parts['scheme'] = strtolower($m[2]);

		if (!empty($m[7])) {
			if (isset($m[9]))
				$parts['user'] = $m[9];
			else
				$parts['user'] = '';
		}
		if (!empty($m[10]))
			$parts['pass'] = $m[11];

		if (!empty($m[13]))
			$h = $parts['host'] = $m[13];
		else if (!empty($m[14]))
			$parts['host'] = $m[14];
		else if (!empty($m[16]))
			$parts['host'] = $m[16];
		else if (!empty($m[5]))
			$parts['host'] = '';
		if (!empty($m[17]))
			$parts['port'] = $m[18];

		if (!empty($m[19]))
			$parts['path'] = $m[19];
		else if (!empty($m[21]))
			$parts['path'] = $m[21];
		else if (!empty($m[25]))
			$parts['path'] = $m[25];

		if (!empty($m[27]))
			$parts['query'] = $m[28];
		if (!empty($m[29]))
			$parts['fragment'] = $m[30];

		if (!$decode)
			return $parts;
		if (!empty($parts['user']))
			$parts['user'] = rawurldecode($parts['user']);
		if (!empty($parts['pass']))
			$parts['pass'] = rawurldecode($parts['pass']);
		if (!empty($parts['path']))
			$parts['path'] = rawurldecode($parts['path']);
		if (isset($h))
			$parts['host'] = rawurldecode($parts['host']);
		if (!empty($parts['query']))
			$parts['query'] = rawurldecode($parts['query']);
		if (!empty($parts['fragment']))
			$parts['fragment'] = rawurldecode($parts['fragment']);
		return $parts;
	}
	
	public static function linkJoin($parts, $encode = TRUE) {
		if ($encode) {
			if (isset($parts['user']))
				$parts['user'] = rawurlencode($parts['user']);
			if (isset($parts['pass']))
				$parts['pass'] = rawurlencode($parts['pass']);
			if (isset($parts['host']) &&
					!preg_match('!^(\[[\da-f.:]+\]])|([\da-f.:]+)$!ui', $parts['host']))
				$parts['host'] = rawurlencode($parts['host']);
			if (!empty($parts['path']))
				$parts['path'] = preg_replace('!%2F!ui', '/', rawurlencode($parts['path']));
			if (isset($parts['query']))
				$parts['query'] = rawurlencode($parts['query']);
			if (isset($parts['fragment']))
				$parts['fragment'] = rawurlencode($parts['fragment']);
		}

		$url = '';
		if (!empty($parts['scheme']))
			$url .= $parts['scheme'] . ':';
		if (isset($parts['host'])) {
			$url .= '//';
			if (isset($parts['user'])) {
				$url .= $parts['user'];
				if (isset($parts['pass']))
					$url .= ':' . $parts['pass'];
				$url .= '@';
			}
			if (preg_match('!^[\da-f]*:[\da-f.:]+$!ui', $parts['host']))
				$url .= '[' . $parts['host'] . ']'; // IPv6
			else
				$url .= $parts['host'];   // IPv4 or name
			if (isset($parts['port']))
				$url .= ':' . $parts['port'];
			if (!empty($parts['path']) && $parts['path'][0] != '/')
				$url .= '/';
		}
		if (!empty($parts['path']))
			$url .= $parts['path'];
		if (isset($parts['query']))
			$url .= '?' . $parts['query'];
		if (isset($parts['fragment']))
			$url .= '#' . $parts['fragment'];
		return $url;
	}	
}

/**
 * @todo Module Helper Class For Pack Javascript
 */
class JavaScriptPacker {
    // constants

    const IGNORE = '$1';

    // validate parameters
    private $_script = '';
    private $_encoding = 62;
    private $_fastDecode = true;
    private $_specialChars = false;
    private $LITERAL_ENCODING = array(
        'None' => 0,
        'Numeric' => 10,
        'Normal' => 62,
        'High ASCII' => 95
    );

    public function __construct($_script, $_encoding = 62, $_fastDecode = true, $_specialChars = false) {
        $this->_script = $_script . "\n";
        if (array_key_exists($_encoding, $this->LITERAL_ENCODING))
            $_encoding = $this->LITERAL_ENCODING[$_encoding];
        $this->_encoding = min((int) $_encoding, 95);
        $this->_fastDecode = $_fastDecode;
        $this->_specialChars = $_specialChars;
    }

    public function pack() {
        $this->_addParser('_basicCompression');
        if ($this->_specialChars)
            $this->_addParser('_encodeSpecialChars');
        if ($this->_encoding)
            $this->_addParser('_encodeKeywords');

        // go!
        return $this->_pack($this->_script);
    }

    // apply all parsing routines
    private function _pack($script) {
        for ($i = 0; isset($this->_parsers[$i]); $i++) {
            $script = call_user_func(array(&$this, $this->_parsers[$i]), $script);
        }
        return $script;
    }

    // keep a list of parsing functions, they'll be executed all at once
    private $_parsers = array();

    private function _addParser($parser) {
        $this->_parsers[] = $parser;
    }

    // zero encoding - just removal of white space and comments
    private function _basicCompression($script) {
        $parser = new ParseMaster();
        // make safe
        $parser->escapeChar = '\\';
        // protect strings
        $parser->add('/\'[^\'\\n\\r]*\'/', self::IGNORE);
        $parser->add('/"[^"\\n\\r]*"/', self::IGNORE);
        // remove comments
        $parser->add('/\\/\\/[^\\n\\r]*[\\n\\r]/', ' ');
        $parser->add('/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//', ' ');
        // protect regular expressions
        $parser->add('/\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)/', '$2'); // IGNORE
        $parser->add('/[^\\w\\x24\\/\'"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?/', self::IGNORE);
        // remove: ;;; doSomething();
        if ($this->_specialChars)
            $parser->add('/;;;[^\\n\\r]+[\\n\\r]/');
        // remove redundant semi-colons
        $parser->add('/\\(;;\\)/', self::IGNORE); // protect for (;;) loops
        $parser->add('/;+\\s*([};])/', '$2');
        // apply the above
        $script = $parser->exec($script);

        // remove white-space
        $parser->add('/(\\b|\\x24)\\s+(\\b|\\x24)/', '$2 $3');
        $parser->add('/([+\\-])\\s+([+\\-])/', '$2 $3');
        $parser->add('/\\s+/', '');
        // done
        return $parser->exec($script);
    }

    private function _encodeSpecialChars($script) {
        $parser = new ParseMaster();
        // replace: $name -> n, $$name -> na
        $parser->add('/((\\x24+)([a-zA-Z$_]+))(\\d*)/', array('fn' => '_replace_name')
        );
        // replace: _name -> _0, double-underscore (__name) is ignored
        $regexp = '/\\b_[A-Za-z\\d]\\w*/';
        // build the word list
        $keywords = $this->_analyze($script, $regexp, '_encodePrivate');
        // quick ref
        $encoded = $keywords['encoded'];

        $parser->add($regexp, array(
            'fn' => '_replace_encoded',
            'data' => $encoded
                )
        );
        return $parser->exec($script);
    }

    private function _encodeKeywords($script) {
        // escape high-ascii values already in the script (i.e. in strings)
        if ($this->_encoding > 62)
            $script = $this->_escape95($script);
        // create the parser
        $parser = new ParseMaster();
        $encode = $this->_getEncoder($this->_encoding);
        // for high-ascii, don't encode single character low-ascii
        $regexp = ($this->_encoding > 62) ? '/\\w\\w+/' : '/\\w+/';
        // build the word list
        $keywords = $this->_analyze($script, $regexp, $encode);
        $encoded = $keywords['encoded'];

        // encode
        $parser->add($regexp, array(
            'fn' => '_replace_encoded',
            'data' => $encoded
                )
        );
        if (empty($script))
            return $script;
        else {
            //$res = $parser->exec($script);
            //$res = $this->_bootStrap($res, $keywords);
            //return $res;
            return $this->_bootStrap($parser->exec($script), $keywords);
        }
    }

    private function _analyze($script, $regexp, $encode) {
        // analyse
        // retreive all words in the script
        $all = array();
        preg_match_all($regexp, $script, $all);
        $_sorted = array(); // list of words sorted by frequency
        $_encoded = array(); // dictionary of word->encoding
        $_protected = array(); // instances of "protected" words
        $all = $all[0]; // simulate the javascript comportement of global match
        if (!empty($all)) {
            $unsorted = array(); // same list, not sorted
            $protected = array(); // "protected" words (dictionary of word->"word")
            $value = array(); // dictionary of charCode->encoding (eg. 256->ff)
            $this->_count = array(); // word->count
            $i = count($all);
            $j = 0; //$word = null;
            // count the occurrences - used for sorting later
            do {
                --$i;
                $word = '$' . $all[$i];
                if (!isset($this->_count[$word])) {
                    $this->_count[$word] = 0;
                    $unsorted[$j] = $word;
                    // make a dictionary of all of the protected words in this script
                    //  these are words that might be mistaken for encoding
                    //if (is_string($encode) && method_exists($this, $encode))
                    $values[$j] = call_user_func(array(&$this, $encode), $j);
                    $protected['$' . $values[$j]] = $j++;
                }
                // increment the word counter
                $this->_count[$word]++;
            } while ($i > 0);
            // prepare to sort the word list, first we must protect
            //  words that are also used as codes. we assign them a code
            //  equivalent to the word itself.
            // e.g. if "do" falls within our encoding range
            //      then we store keywords["do"] = "do";
            // this avoids problems when decoding
            $i = count($unsorted);
            do {
                $word = $unsorted[--$i];
                if (isset($protected[$word]) /* != null */) {
                    $_sorted[$protected[$word]] = substr($word, 1);
                    $_protected[$protected[$word]] = true;
                    $this->_count[$word] = 0;
                }
            } while ($i);

            // sort the words by frequency
            // Note: the javascript and php version of sort can be different :
            // in php manual, usort :
            // " If two members compare as equal,
            // their order in the sorted array is undefined."
            // so the final packed script is different of the Dean's javascript version
            // but equivalent.
            // the ECMAscript standard does not guarantee this behaviour,
            // and thus not all browsers (e.g. Mozilla versions dating back to at
            // least 2003) respect this. 
            usort($unsorted, array(&$this, '_sortWords'));
            $j = 0;
            // because there are "protected" words in the list
            //  we must add the sorted words around them
            do {
                if (!isset($_sorted[$i]))
                    $_sorted[$i] = substr($unsorted[$j++], 1);
                $_encoded[$_sorted[$i]] = $values[$i];
            } while (++$i < count($unsorted));
        }
        return array(
            'sorted' => $_sorted,
            'encoded' => $_encoded,
            'protected' => $_protected);
    }

    private $_count = array();

    private function _sortWords($match1, $match2) {
        return $this->_count[$match2] - $this->_count[$match1];
    }

    // build the boot function used for loading and decoding
    private function _bootStrap($packed, $keywords) {
        $ENCODE = $this->_safeRegExp('$encode\\($count\\)');

        // $packed: the packed script
        $packed = "'" . $this->_escape($packed) . "'";

        // $ascii: base for encoding
        $ascii = min(count($keywords['sorted']), $this->_encoding);
        if ($ascii == 0)
            $ascii = 1;

        // $count: number of words contained in the script
        $count = count($keywords['sorted']);

        // $keywords: list of words contained in the script
        foreach ($keywords['protected'] as $i => $value) {
            $keywords['sorted'][$i] = '';
        }
        // convert from a string to an array
        ksort($keywords['sorted']);
        $keywords = "'" . implode('|', $keywords['sorted']) . "'.split('|')";

        $encode = ($this->_encoding > 62) ? '_encode95' : $this->_getEncoder($ascii);
        $encode = $this->_getJSFunction($encode);
        $encode = preg_replace('/_encoding/', '$ascii', $encode);
        $encode = preg_replace('/arguments\\.callee/', '$encode', $encode);
        $inline = '\\$count' . ($ascii > 10 ? '.toString(\\$ascii)' : '');

        // $decode: code snippet to speed up decoding
        if ($this->_fastDecode) {
            // create the decoder
            $decode = $this->_getJSFunction('_decodeBody');
            if ($this->_encoding > 62)
                $decode = preg_replace('/\\\\w/', '[\\xa1-\\xff]', $decode);
            // perform the encoding inline for lower ascii values
            elseif ($ascii < 36)
                $decode = preg_replace($ENCODE, $inline, $decode);
            // special case: when $count==0 there are no keywords. I want to keep
            //  the basic shape of the unpacking funcion so i'll frig the code...
            if ($count == 0)
                $decode = preg_replace($this->_safeRegExp('($count)\\s*=\\s*1'), '$1=0', $decode, 1);
        }

        // boot function
        $unpack = $this->_getJSFunction('_unpack');
        if ($this->_fastDecode) {
            // insert the decoder
            $this->buffer = $decode;
            $unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastDecode'), $unpack, 1);
        }
        $unpack = preg_replace('/"/', "'", $unpack);
        if ($this->_encoding > 62) { // high-ascii
            // get rid of the word-boundaries for regexp matches
            $unpack = preg_replace('/\'\\\\\\\\b\'\s*\\+|\\+\s*\'\\\\\\\\b\'/', '', $unpack);
        }
        if ($ascii > 36 || $this->_encoding > 62 || $this->_fastDecode) {
            // insert the encode function
            $this->buffer = $encode;
            $unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastEncode'), $unpack, 1);
        } else {
            // perform the encoding inline
            $unpack = preg_replace($ENCODE, $inline, $unpack);
        }
        // pack the boot function too
        $unpackPacker = new JavaScriptPacker($unpack, 0, false, true);
        $unpack = $unpackPacker->pack();

        // arguments
        $params = array($packed, $ascii, $count, $keywords);
        if ($this->_fastDecode) {
            $params[] = 0;
            $params[] = '{}';
        }
        $params = implode(',', $params);

        // the whole thing
        return 'eval(' . $unpack . '(' . $params . "))\n";
    }

    private $buffer;

    private function _insertFastDecode($match) {
        return '{' . $this->buffer . ';';
    }

    private function _insertFastEncode($match) {
        return '{$encode=' . $this->buffer . ';';
    }

    // mmm.. ..which one do i need ??
    private function _getEncoder($ascii) {
        return $ascii > 10 ? $ascii > 36 ? $ascii > 62 ?
                                '_encode95' : '_encode62'  : '_encode36'  : '_encode10';
    }

    // zero encoding
    // characters: 0123456789
    private function _encode10($charCode) {
        return $charCode;
    }

    // inherent base36 support
    // characters: 0123456789abcdefghijklmnopqrstuvwxyz
    private function _encode36($charCode) {
        return base_convert($charCode, 10, 36);
    }

    // hitch a ride on base36 and add the upper case alpha characters
    // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
    private function _encode62($charCode) {
        $res = '';
        if ($charCode >= $this->_encoding) {
            $res = $this->_encode62((int) ($charCode / $this->_encoding));
        }
        $charCode = $charCode % $this->_encoding;

        if ($charCode > 35)
            return $res . chr($charCode + 29);
        else
            return $res . base_convert($charCode, 10, 36);
    }

    // use high-ascii values
    // characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
    private function _encode95($charCode) {
        $res = '';
        if ($charCode >= $this->_encoding)
            $res = $this->_encode95($charCode / $this->_encoding);

        return $res . chr(($charCode % $this->_encoding) + 161);
    }

    private function _safeRegExp($string) {
        return '/' . preg_replace('/\$/', '\\\$', $string) . '/';
    }

    private function _encodePrivate($charCode) {
        return "_" . $charCode;
    }

    // protect characters used by the parser
    private function _escape($script) {
        return preg_replace('/([\\\\\'])/', '\\\$1', $script);
    }

    // protect high-ascii characters already in the script
    private function _escape95($script) {
        return preg_replace_callback(
                        '/[\\xa1-\\xff]/', array(&$this, '_escape95Bis'), $script
        );
    }

    private function _escape95Bis($match) {
        return '\x' . ((string) dechex(ord($match)));
    }

    private function _getJSFunction($aName) {
        if (defined('self::JSFUNCTION' . $aName))
            return constant('self::JSFUNCTION' . $aName);
        else
            return '';
    }

    // JavaScript Functions used.
    // Note : In Dean's version, these functions are converted
    // with 'String(aFunctionName);'.
    // This internal conversion complete the original code, ex :
    // 'while (aBool) anAction();' is converted to
    // 'while (aBool) { anAction(); }'.
    // The JavaScript functions below are corrected.
    // unpacking function - this is the boot strap function
    //  data extracted from this packing routine is passed to
    //  this function when decoded in the target
    // NOTE ! : without the ';' final.

    const JSFUNCTION_unpack =
            'function($packed, $ascii, $count, $keywords, $encode, $decode) {
    while ($count--) {
        if ($keywords[$count]) {
            $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
        }
    }
    return $packed;
}';
    /*
      'function($packed, $ascii, $count, $keywords, $encode, $decode) {
      while ($count--)
      if ($keywords[$count])
      $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
      return $packed;
      }';
     */

    // code-snippet inserted into the unpacker to speed up decoding
    const JSFUNCTION_decodeBody =
//_decode = function() {
// does the browser support String.replace where the
//  replacement value is a function?

            '    if (!\'\'.replace(/^/, String)) {
        // decode all the values we need
        while ($count--) {
            $decode[$encode($count)] = $keywords[$count] || $encode($count);
        }
        // global replacement function
        $keywords = [function ($encoded) {return $decode[$encoded]}];
        // generic match
        $encode = function () {return \'\\\\w+\'};
        // reset the loop counter -  we are now doing a global replace
        $count = 1;
    }
';
//};
    /*
      '	if (!\'\'.replace(/^/, String)) {
      // decode all the values we need
      while ($count--) $decode[$encode($count)] = $keywords[$count] || $encode($count);
      // global replacement function
      $keywords = [function ($encoded) {return $decode[$encoded]}];
      // generic match
      $encode = function () {return\'\\\\w+\'};
      // reset the loop counter -  we are now doing a global replace
      $count = 1;
      }';
     */

    // zero encoding
    // characters: 0123456789
    const JSFUNCTION_encode10 =
            'function($charCode) {
    return $charCode;
}'; //;';
    // inherent base36 support
    // characters: 0123456789abcdefghijklmnopqrstuvwxyz
    const JSFUNCTION_encode36 =
            'function($charCode) {
    return $charCode.toString(36);
}'; //;';
    // hitch a ride on base36 and add the upper case alpha characters
    // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
    const JSFUNCTION_encode62 =
            'function($charCode) {
    return ($charCode < _encoding ? \'\' : arguments.callee(parseInt($charCode / _encoding))) +
    (($charCode = $charCode % _encoding) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
}';

    // use high-ascii values
    // characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
    const JSFUNCTION_encode95 =
            'function($charCode) {
    return ($charCode < _encoding ? \'\' : arguments.callee($charCode / _encoding)) +
        String.fromCharCode($charCode % _encoding + 161);
}';

}

/**
 * @todo Module Helper Class For Pack Javascript
 */
class ParseMaster {

    public $ignoreCase = false;
    public $escapeChar = '';

    // constants

    const EXPRESSION = 0;
    const REPLACEMENT = 1;
    const LENGTH = 2;

    // used to determine nesting levels
    private $GROUPS = '/\\(/'; //g
    private $SUB_REPLACE = '/\\$\\d/';
    private $INDEXED = '/^\\$\\d+$/';
    private $TRIM = '/([\'"])\\1\\.(.*)\\.\\1\\1$/';
    private $ESCAPE = '/\\\./'; //g
    private $QUOTE = '/\'/';
    private $DELETED = '/\\x01[^\\x01]*\\x01/'; //g

    public function add($expression, $replacement = '') {
        // count the number of sub-expressions
        //  - add one because each pattern is itself a sub-expression
        $length = 1 + preg_match_all($this->GROUPS, $this->_internalEscape((string) $expression), $out);

        // treat only strings $replacement
        if (is_string($replacement)) {
            // does the pattern deal with sub-expressions?
            if (preg_match($this->SUB_REPLACE, $replacement)) {
                // a simple lookup? (e.g. "$2")
                if (preg_match($this->INDEXED, $replacement)) {
                    // store the index (used for fast retrieval of matched strings)
                    $replacement = (int) (substr($replacement, 1)) - 1;
                } else { // a complicated lookup (e.g. "Hello $2 $1")
                    // build a function to do the lookup
                    $quote = preg_match($this->QUOTE, $this->_internalEscape($replacement)) ? '"' : "'";
                    $replacement = array(
                        'fn' => '_backReferences',
                        'data' => array(
                            'replacement' => $replacement,
                            'length' => $length,
                            'quote' => $quote
                        )
                    );
                }
            }
        }
        // pass the modified arguments
        if (!empty($expression))
            $this->_add($expression, $replacement, $length);
        else
            $this->_add('/^$/', $replacement, $length);
    }

    public function exec($string) {
        // execute the global replacement
        $this->_escaped = array();

        // simulate the _patterns.toSTring of Dean
        $regexp = '/';
        foreach ($this->_patterns as $reg) {
            $regexp .= '(' . substr($reg[self::EXPRESSION], 1, -1) . ')|';
        }
        $regexp = substr($regexp, 0, -1) . '/';
        $regexp .= ($this->ignoreCase) ? 'i' : '';

        $string = $this->_escape($string, $this->escapeChar);
        $string = preg_replace_callback(
                $regexp, array(
            &$this,
            '_replacement'
                ), $string
        );
        $string = $this->_unescape($string, $this->escapeChar);

        return preg_replace($this->DELETED, '', $string);
    }

    public function reset() {
        // clear the patterns collection so that this object may be re-used
        $this->_patterns = array();
    }

    // private
    private $_escaped = array();  // escaped characters
    private $_patterns = array(); // patterns stored by index

    // create and add a new pattern to the patterns collection

    private function _add() {
        $arguments = func_get_args();
        $this->_patterns[] = $arguments;
    }

    // this is the global replace function (it's quite complicated)
    private function _replacement($arguments) {
        if (empty($arguments))
            return '';

        $i = 1;
        $j = 0;
        // loop through the patterns
        while (isset($this->_patterns[$j])) {
            $pattern = $this->_patterns[$j++];
            // do we have a result?
            if (isset($arguments[$i]) && ($arguments[$i] != '')) {
                $replacement = $pattern[self::REPLACEMENT];

                if (is_array($replacement) && isset($replacement['fn'])) {

                    if (isset($replacement['data']))
                        $this->buffer = $replacement['data'];
                    return call_user_func(array(&$this, $replacement['fn']), $arguments, $i);
                } elseif (is_int($replacement)) {
                    return $arguments[$replacement + $i];
                }
                $delete = ($this->escapeChar == '' ||
                        strpos($arguments[$i], $this->escapeChar) === false) ? '' : "\x01" . $arguments[$i] . "\x01";
                return $delete . $replacement;

                // skip over references to sub-expressions
            } else {
                $i += $pattern[self::LENGTH];
            }
        }
    }

    private function _backReferences($match, $offset) {
        $replacement = $this->buffer['replacement'];
        $quote = $this->buffer['quote'];
        $i = $this->buffer['length'];
        while ($i) {
            $replacement = str_replace('$' . $i--, $match[$offset + $i], $replacement);
        }
        return $replacement;
    }

    private function _replace_name($match, $offset) {
        $length = strlen($match[$offset + 2]);
        $start = $length - max($length - strlen($match[$offset + 3]), 0);
        return substr($match[$offset + 1], $start, $length) . $match[$offset + 4];
    }

    private function _replace_encoded($match, $offset) {
        return $this->buffer[$match[$offset]];
    }

    // php : we cannot pass additional data to preg_replace_callback,
    // and we cannot use &$this in create_function, so let's go to lower level
    private $buffer;

    // encode escaped characters
    private function _escape($string, $escapeChar) {
        if ($escapeChar) {
            $this->buffer = $escapeChar;
            return preg_replace_callback(
                            '/\\' . $escapeChar . '(.)' . '/', array(&$this, '_escapeBis'), $string
            );
        } else {
            return $string;
        }
    }

    private function _escapeBis($match) {
        $this->_escaped[] = $match[1];
        return $this->buffer;
    }

    // decode escaped characters
    private function _unescape($string, $escapeChar) {
        if ($escapeChar) {
            $regexp = '/' . '\\' . $escapeChar . '/';
            $this->buffer = array('escapeChar' => $escapeChar, 'i' => 0);
            return preg_replace_callback
                            (
                            $regexp, array(&$this, '_unescapeBis'), $string
            );
        } else {
            return $string;
        }
    }

    private function _unescapeBis() {
        if (isset($this->_escaped[$this->buffer['i']])
                && $this->_escaped[$this->buffer['i']] != '') {
            $temp = $this->_escaped[$this->buffer['i']];
        } else {
            $temp = '';
        }
        $this->buffer['i']++;
        return $this->buffer['escapeChar'] . $temp;
    }

    private function _internalEscape($string) {
        return preg_replace($this->ESCAPE, '', $string);
    }

}